﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml.XPath;
using Mono.Cecil;
using Newtonsoft.Json;
using Skyline;
using SmartQuant;

namespace PluginInstaller
{
    internal class Installer
    {
        private readonly string _infoFile;
        private PluginInfo[] _infos;

        private void LoadPluginInfo(string infoFile)
        {
            _infos = JsonConvert.DeserializeObject<PluginInfo[]>(File.ReadAllText(infoFile));
        }

        private static string GetConfigurationPath()
        {
            return Path.Combine(Installation.ConfigDir.FullName, "configuration.xml");
        }

        private static bool IsProivder(TypeDefinition type)
        {
            var baseType = type.BaseType;
            while (baseType != null) {
                if (baseType.Name == "XProvider") {
                    return true;
                }

                baseType = baseType.Resolve().BaseType;
            }

            return false;
        }

        private static List<string> GetProviders(string name)
        {
            var list = new List<string>();
            var asm = AssemblyDefinition.ReadAssembly(name);
            foreach (var type in asm.MainModule.Types) {
                if (IsProivder(type)) {
                    list.Add(type.Name);
                }
            }

            return list;
        }

        private void UpdateOpenQuantAppConfig(PluginInfo info)
        {
            ShowMessage(@"正在更新 OpenQuant.exe.config");

            var path = Path.Combine(PInstallerProgram.SmartQuantPath, "OpenQuant.exe.config");
            if (!File.Exists(path)) {
                File.WriteAllText(path, Resource.OQAppConfig);
            }

            var helper = new AppConfigHelper(path);
            if (!string.IsNullOrEmpty(info.PrivatePath)) {
                helper.SetPrivatePath(info.PrivatePath);
            }

            foreach (var item in info.DependentAssemblyList) {
                var file = item.File;
                if (!File.Exists(file)) {
                    file = Path.Combine(PInstallerProgram.SmartQuantPath, file);
                }

                var asm = AssemblyDefinition.ReadAssembly(file).Name;
                var token = asm.HasPublicKey ? Utility.BytesToHexString(asm.PublicKeyToken).ToLower() : string.Empty;
                var version = asm.Version;
                if (!string.IsNullOrEmpty(item.Version)) {
                    var request = Version.Parse(item.Version);
                    version = request > version ? request : version;
                }

                helper.SetDependentAssembly(Path.GetFileNameWithoutExtension(file), token, version.ToString(),
                    asm.Version.ToString());
            }

            helper.Save();

            ShowMessage(@"OpenQuant.exe.config 更新完毕");
        }

        private void UpdateProviderInfo(string path, PluginInfo info)
        {
            ShowMessage(@"正在写入插件信息");

            var doc = XDocument.Load(path);
            var typeNames = doc.XPathSelectElements("/Configuration/Providers/Provider/TypeName");
            var installed = false;
            ParseTypeName(info.ProviderTypeName, out var installedTypeName, out var installedAsmName);
            var providers = GetProviders(installedAsmName + ".dll");
            foreach (var element in typeNames) {
                ParseTypeName(element.Value, out var typeName, out var asmName);
                if (asmName != installedAsmName)
                    continue;
                if (installedTypeName != typeName && providers.Count != 1)
                    continue;
                element.Value = info.ProviderTypeName;
                installed = true;
                break;
            }

            if (!installed) {
                var element = doc.XPathSelectElement("/Configuration/Providers");
                if (element != null) {
                    var item = new XElement("Provider");
                    item.Add(new XElement("TypeName") { Value = info.ProviderTypeName });
                    item.Add(new XElement("Id") { Value = "0" });
                    item.Add(new XElement("X64") { Value = "true" });
                    element.Add(item);
                }
            }

            doc.Save(path);
        }

        private static void ParseTypeName(string name, out string typeName, out string asmName)
        {
            typeName = string.Empty;
            asmName = string.Empty;
            var items = name.Split(',');
            if (items.Length != 2)
                return;
            typeName = items[0].Trim();
            asmName = items[1].Trim();
        }

        private void InitOpenQuant()
        {
            ShowMessage(@"正在初始化 OpenQuant");
            var serializer = new XmlSerializer(typeof(Configuration));
            var file = GetConfigurationPath();
            var dir = Path.GetDirectoryName(file);
            if (dir != null && !Directory.Exists(dir)) {
                Directory.CreateDirectory(dir);
            }

            var writer = new StreamWriter(file);
            serializer.Serialize(writer, Configuration.DefaultConfiguaration());
            writer.Close();
            ShowMessage(@"OpenQuant 初始化完毕");
        }

        public Installer(string infoFile)
        {
            _infoFile = infoFile;
        }

        public void Install()
        {
            try {
                LoadPluginInfo(_infoFile);
                foreach (var info in _infos) {
                    ShowMessage($"开始安装 {info.ProviderName}");
                    var path = GetConfigurationPath();
                    if (!File.Exists(path)) {
                        InitOpenQuant();
                    }
                    UpdateProviderInfo(path, info);
                    UpdateOpenQuantAppConfig(info);
                    ShowMessage($"{info.ProviderName} 安装完成");
                }
                Done(true);
            }
            catch (Exception ex) {
                ShowMessage(ex.ToString());
                Done(false);
            }
        }

        public Action<string> ShowMessage { get; set; } = Console.WriteLine;
        public Action<bool> Done { get; set; } = _ => { };
    }
}